#!/usr/bin/env python3
import sys

_type_2_chunk = {}

def ByteLike(clazz):
    class Result(clazz):
        def parse_uint(self, bytes=2):
            result = int.from_bytes(self._bytes[self._position : self._position + bytes], byteorder='little')
            self._position += bytes
            return result

        def parse_u16int(self):
            return self.parse_uint()

        def parse_u32int(self):
            return self.parse_uint(4)

        def parse_u8int(self):
            return self.parse_uint(1)

    return Result

def Chunk(clazz):
    @ByteLike
    class Result(clazz):
        def parse_header(self, contain_type=True):
            if contain_type:
                self.header_type = self.parse_u16int()
            self.header_size = self.parse_u16int()
            self.size = self.parse_u32int()

        def origin_clazz(self):
            if hasattr(super(), 'origin_clazz'):
                return super().origin_clazz()
            else:
                return clazz


    _type_2_chunk[clazz.RES_TYPE] = Result
    return Result

@Chunk
class ResTable:
    RES_TYPE = 0x0002

    def parse(self, buffer):
        self._position = 0
        self._bytes = buffer
        self.parse_header()
        if self.header_type != ResTable.RES_TYPE:
            raise Exception('Not valid resource.asrc file: ' + str(hex(self.header_type)))
        self.package_count = self.parse_uint(4)
        next_type = self.parse_u16int()
        if next_type != ResStringPoolType.RES_TYPE:
            raise Exception('Not Valid Format, and String Pool TYPE: ' + str(hex(next_type)))
        self.global_string_pool = ResStringPoolType()
        self.global_string_pool.parse(self._bytes, self._position)
        self._position += self.global_string_pool.size - 2
        self.packages = [self.generate_package() for index in range(self.package_count)]

    def generate_package(self):
        next_type = self.parse_u16int()
        if next_type != ResTablePackage.RES_TYPE:
            raise Exception('not valid package type: ' + str(hex(next_type)))
        package = ResTablePackage()
        package.parse(self._bytes, self._position)
        self._position += package.size - 2
        return package
        
    def show(self):
        print(f"package_count: {self.package_count}, and current_position: {self._position}")
        self.global_string_pool.show()
        for package in self.packages:
            package.show()

@Chunk
class ResStringPoolType:
    RES_TYPE = 0x0001

    def parse(self, bytes, start_index):
        self._bytes = bytes
        self._position = start_index
        self.parse_header(contain_type=False)
        self.string_count = self.parse_u32int()
        self.style_count = self.parse_u32int()
        self.flags = self.parse_u32int()
        self.strings_start = self.parse_u32int()
        self.styles_start = self.parse_u32int()
        self.string_offsets = [self.parse_u32int() for x in range(self.string_count)]
        self.style_offsets = [self.parse_u32int() for x in range(self.style_count)]
        self._is_utf8 = self.flags & (1 << 8) != 0
        chunk_start = start_index - 2
        self.strings = [self._parse_string(chunk_start + self.strings_start + self.string_offsets[x]) for x in range(self.string_count)]
        self._bytes = None

    def _cal_length_utf8(self, start_index):
        self._position = start_index
        length = self._bytes[self._position]
        if length & 0x80 != 0:
            length = ((length & 0x7F) << 8 ) | self._bytes[self._position + 1]
            self._position += 2
        else:
            self._position += 1
        return length

    def _cal_length_utf16(self, start_index):
        self._position = start_index
        length = self.parse_u16int()
        if length & 0x80000 != 0:
            length = ((length & 0x7FFF) << 16 ) | self.parse_u16int()
        return length

    def _parse_string(self, str_start):
        self._position = str_start
        if self._is_utf8:
            character_count = self._cal_length_utf8(self._position)
            byte_count = self._cal_length_utf8(self._position)
        else:
            character_count = self._cal_length_utf16(self._position)
            byte_count = character_count * 2
        end = self._position + byte_count
        str_bytes = self._bytes[self._position: end]
        return str_bytes.decode('utf-8' if self._is_utf8 else 'UTF-16LE')

    def show(self):
        print(f'StringPool: len: {len(self.strings)},:' + ('utf8' if self._is_utf8 else 'utf16'))
#        print(str(self.strings))
        
        

@Chunk
class ResTablePackage:
    RES_TYPE = 0x0200
    PACKAGE_NAME_SIZE = 256

    def parse(self, bytes, start_index):
        self._bytes = bytes
        self._position = start_index
        self.parse_header(contain_type=False)
        self.package_id = self.parse_u32int()
        self.package_name = self._read_package_name(self._bytes[self._position:self._position + 256])
        self._position += 256
        self.type_string_pool_offset = self.parse_u32int()
        self.last_public_type = self.parse_u32int()
        self.key_string_pool_offset = self.parse_u32int()
        self.last_public_key = self.parse_u32int()
        self.typed_id_offset = self.parse_u32int()
        self._parse_chunks(start_index - 2)
        self._bytes = None

    def _parse_chunks(self, start):
        self.chunks = []
        self.offset_2_chunk = {}
        self._position = start + self.header_size
        end = start + self.size
        while self._position < end:
            start_position = self._position
            next_type = self.parse_u16int()
            chunk_type = _type_2_chunk[next_type]
            chunk = chunk_type()
            chunk.parse(self._bytes, self._position)
            self._position += chunk.size - 2
            self.chunks.append(chunk)
            self.offset_2_chunk[start_position - start] = chunk

        for chunk in self.chunks:
            if hasattr(chunk, 'with_package'):
                chunk.with_package(self)

    def get_type_name_by_id(self, type_id):
        return self.offset_2_chunk[self.type_string_pool_offset].strings[type_id - 1]

    def _read_package_name(self, name_bytes):
        length = name_bytes.index(b'\x00\x00')
        content = name_bytes[:length] + b'\x00'
        return content.decode('UTF-16LE')

    def show(self):
        print(f"\npackage name: {self.package_name}")
        print(f"chunks:")
        for index, chunk in enumerate(self.chunks):
            if hasattr(chunk, 'show'):
                chunk.show()
            else:
                if index % 5 == 0 and index != 0:
                    print()
                print(str(chunk.origin_clazz().__name__).rjust(18), end='')
        print('')

@Chunk
class ResTableTypeSpec:
    RES_TYPE = 0x0202

    def parse(self, bytes, start_index):
        self._position = start_index
        self._bytes = bytes
        self.parse_header(contain_type=False)
        self.type_id = self._bytes[self._position]
        self._position += 3
        self.resource_count = self.parse_u32int()
        self.resources = [self.parse_u32int() for i in range(self.resource_count)]
        self._bytes = None

    def with_package(self, package):
        self._type_id_name = package.get_type_name_by_id(self.type_id)
        
    def show(self):
        print(f'\nResTableTypeSpec: type: {self._type_id_name} resource_count: {self.resource_count}\n')

@Chunk
class ResTableType:
    RES_TYPE = 0x0201

    def parse(self, bytes, start_index):
        self._position = start_index
        self._bytes = bytes
        self.parse_header(contain_type=False)
        self.type_id = self._bytes[self._position]
        self._position += 3
        self.entry_count = self.parse_u32int()
        self.entry_start = self.parse_u32int()
        self.config = Configuration(self._bytes, self._position)

        self._position = start_index - 2 + self.entry_start
        self.entrys = [ResourceValue.create(self) for index in range(self.entry_count)]
        self._bytes = None
        
@ByteLike
class Configuration:
    SCREEN_CONFIG_MIN_SIZE = 32  # The minimum size in bytes that this configuration must be to contain screen config info.
    SCREEN_DP_MIN_SIZE = 36      # The minimum size in bytes that this configuration must be to contain screen dp info.
    LOCALE_MIN_SIZE = 48         # The minimum size in bytes that this configuration must be to contain locale info.
    SCREEN_CONFIG_EXTENSION_MIN_SIZE = 52  # The xxxxxx that this configuration must be to contain the screenConfig extension. 
    
    def __init__(self, bytes, start_index):
        self._bytes = bytes
        self._position = start_index
        self.size = self.parse_u32int()
        self.mcc = self.parse_u16int() 
        self.mnc = self.parse_u16int()
        self.language = self._bytes[self._position : self._position + 2]
        self._position += 2
        self.region = self._bytes[self._position : self._position + 2]
        self._position += 2
        self.orientation = self.parse_byte()
        self.touchscreen = self.parse_byte()
        self.density = self.parse_u16int()
        self.keyboard = self.parse_byte()
        self.navigation = self.parse_byte()
        self.input_flags = self.parse_byte()
        self._position += 1
        self.screen_width = self.parse_u16int()
        self.screen_height = self.parse_u16int()
        self.sdk_version = self.parse_u16int()
        self.minor_version = self.parse_u16int()
        
        self.screen_layout = 0
        self.ui_mode = 0
        self.smallest_screen_width_dp = 0
        self.screen_width_dp = 0
        self.screen_height_dp = 0
        self.locale_script = None
        self.locale_variant = None
        if self.size >= Configuration.SCREEN_CONFIG_MIN_SIZE:
            self.screen_layout = self.parse_byte()
            self.ui_mode = self.parse_byte()
            self.smallest_screen_width_dp = self.parse_u16int()

        if self.size >= Configuration.SCREEN_DP_MIN_SIZE:
            self.screen_width_dp = self.parse_u16int()
            self.screen_height_dp = self.parse_u16int()

        if self.size >= Configuration.LOCALE_MIN_SIZE:
            self.locale_script = self._bytes[self._position : self._position + 4]
            self._position += 4
            self.locale_variant = self._bytes[self._position : self._position + 8]
            self._position += 8

        if self.size >= Configuration.SCREEN_CONFIG_MIN_SIZE:
            self.screen_layout = self.parse_byte()
            self._position += 3
        del self._bytes

    def parse_byte(self):
        self._position += 1
        return self._bytes[self._position - 1]
@Chunk
class ResTableLibrary:
    RES_TYPE = 0x0203

class ResourceValue:

    def __init__(self, size, type, data):
        self.size = size
        self.type = type
        self.data = data

    @staticmethod
    def create(bytelike):
        size = bytelike.parse_u16int()
        bytelike.parse_u8int()
        type = bytelike.parse_u8int()
        data = bytelike.parse_u32int()
        return ResourceValue(size, type, data)


if __name__ == '__main__':
    if len(sys.argv) > 2:
        filepath = sys.argv[1]
    else:
        filepath = 'resources.arsc'

    with open(filepath, 'rb') as file:
        buffer = file.read()

    table = ResTable()
    table.parse(buffer)
    table.show()
    
        
